import java.util.ArrayList;

import java.io.*;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;

/*
 * Загрузка AVL дерева из бинарного файла. Структура файла идентичная JSON - массив массивов.
 */
public class TreeBinaryReaderWriter<T> implements TreeReader<T> {

    final MappedByteBuffer mbb;
    final ArrayList<Object> data = new ArrayList<Object>(10);
    int id = 0;
    boolean isDelimiter = false;
    int lineNumber = 0;
    private static Charset UTF8 = Charset.forName("UTF-8");

    TreeBinaryReaderWriter(MappedByteBuffer mbb) {
        this.mbb = mbb;
    }

    @Override
    final public boolean next() throws IOException {
        if (mbb.position() >= mbb.limit()) {
            return false;
        }
        id = mbb.getInt();
        isDelimiter = (id == 0);

        data.clear();
        lineNumber++;
        return true;
    }

    @Override
    final public boolean isDelimiter() {
        return isDelimiter;
    }

    @Override
    public ArrayList<Object> parse1() {
        try {
            int left = mbb.getInt();
            int right = mbb.getInt();
            int parent = mbb.getInt();
            byte[] key = readByteArray(mbb);
            data.add(id);
            data.add((left != 0) ? left : null);
            data.add((right != 0) ? right : null);
            data.add((parent != 0) ? parent : null);
            data.add(new Word(key));
        } catch (IOException e) {
            throw new UnsupportedOperationException(e);
        }
        return data;
    }

    @Override
    final public ArrayList<Object> parseWord() {
        try {
            WordInterface value = WordInterface.fromBinary(mbb);
            data.add(value);
        } catch (IOException e) {
            throw new UnsupportedOperationException(e);
        }
        return data;
    }

    @Override
    final public ArrayList<Object> parseDataArray() {
        try {
            Object[] refvalue = readIntegerArray(mbb);
            Object[] refdata = readStringArrayDelayed(mbb);
            data.add(refvalue);
            data.add(refdata);
        } catch (IOException e) {
            throw new UnsupportedOperationException(e);
        }
        return data;
    }

    @Override
    final public ArrayList<Object> parseIndexArray() {
        try {
            Object[] index = readBigIntegerArray(mbb);
            data.add(index);
        } catch (IOException e) {
            throw new UnsupportedOperationException(e);
        }
        return data;
    }

    public static void writeArray(DataOutputStream dos, byte[] array) throws IOException {
        if (array.length > 255)
            throw new UnsupportedOperationException();

        dos.writeByte(array.length);
        for (byte b : array) {
            dos.writeByte(b);
        }
    }

    public static void writeIntegerArray(DataOutputStream dos, Object[] array) throws IOException {
        if (array.length > 65535)
            throw new UnsupportedOperationException();

        dos.writeChar(array.length);
        for (Object i : array) {
            dos.writeInt((Integer)i);
        }
    }

    public static void writeBigIntegerArray(DataOutputStream dos, Object[] array) throws IOException {
        dos.writeInt(array.length);
        for (Object i : array) {
            dos.writeInt((Integer)i);
        }
    }

    public static void writeStringArray(DataOutputStream dos, String[] array) throws IOException {
        if (array.length > 65535)
            throw new UnsupportedOperationException();

        for (String str : array) {
            writeUTF(dos, str);
        }
    }

    public static byte[] readByteArray(MappedByteBuffer mbb) throws IOException {
        final int len = (int) mbb.get() & 0xFF;        // byte в беззнаковое целое
        final byte[] d = new byte[len];
        for (int i = 0; i < len; i++) {
            d[i] = mbb.get();
        }
        return d;
    }

    public static Object[] readIntegerArray(MappedByteBuffer mbb) throws IOException {
        final int len = mbb.getChar();
        final Object[] d = new Object[len];
        for (int i = 0; i < len; i++) {
            d[i] = mbb.getInt();
        }
        return d;
    }
    
    public static Object[] readBigIntegerArray(MappedByteBuffer mbb) throws IOException {
        final int len = mbb.getInt();
        final Object[] d = new Object[len];
        for (int i = 0; i < len; i++) {
            d[i] = mbb.getInt();
        }
        return d;
    }

    public static Object[] readStringArrayDelayed(MappedByteBuffer mbb) throws IOException {
        final int str_len = mbb.getChar();
        if (str_len != 0) {
            final Integer[] d = new Integer[3];
            d[0] = (int) str_len;   // размер массива в числе строк
            d[1] = mbb.getInt();    // pos - откуда читать массив
            d[2] = mbb.getInt();    // len - размер массива строк в байтах
            return d;
        } else {
            return null;
        }
    }

    public static Object[] readStringArray(final String filename, final int str_len, final int pos, final int len) throws IOException {
        RandomAccessFile raf = new RandomAccessFile(filename, "r");
        MappedByteBuffer mbb = raf.getChannel().map(FileChannel.MapMode.READ_ONLY, pos, len); // сдигаемся для чтения числа элементов в массиве
        final String[] d = new String[str_len];
        for (int i = 0; i < str_len; i++) {
            d[i] = readUTF(mbb);
        }
        raf.close();
        return d;
    }

    static private void writeUTF(DataOutputStream dos, String str) throws IOException {
        if (str != null) {
            byte[] b_str = str.getBytes(UTF8);
            dos.writeChar((char) b_str.length);
            for (byte b : b_str)
                dos.writeByte(b);
        } else {
            dos.writeChar(0);
        }
    }

    static private String readUTF(MappedByteBuffer buffer) {
        int l = buffer.getChar();
        if (l != 0) {
            byte[] b = new byte[l];
            buffer.get(b, 0, l);
            return new String(b, StandardCharsets.UTF_8);
        } else {
            return null;
        }
    }
}